home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 748 / 748.xpi / content / config.js < prev    next >
Text File  |  2010-02-11  |  21KB  |  643 lines

  1. function Config() {
  2.   this._scripts = null;
  3.   this._configFile = this._scriptDir;
  4.   this._configFile.append("config.xml");
  5.   this._initScriptDir();
  6.  
  7.   this._observers = [];
  8.  
  9.   this._updateVersion();
  10.   this._load();
  11. }
  12.  
  13. Config.prototype = {
  14.   addObserver: function(observer, script) {
  15.     var observers = script ? script._observers : this._observers;
  16.     observers.push(observer);
  17.   },
  18.  
  19.   removeObserver: function(observer, script) {
  20.     var observers = script ? script._observers : this._observers;
  21.     var index = observers.indexOf(observer);
  22.     if (index == -1) throw new Error("Observer not found");
  23.     observers.splice(index, 1);
  24.   },
  25.  
  26.   _notifyObservers: function(script, event, data) {
  27.     var observers = this._observers.concat(script._observers);
  28.     for (var i = 0, observer; observer = observers[i]; i++) {
  29.       observer.notifyEvent(script, event, data);
  30.     }
  31.   },
  32.  
  33.   _changed: function(script, event, data) {
  34.     this._save();
  35.     this._notifyObservers(script, event, data);
  36.   },
  37.  
  38.   installIsUpdate: function(script) {
  39.     return this._find(script) > -1;
  40.   },
  41.  
  42.   _find: function(aScript) {
  43.     namespace = aScript._namespace.toLowerCase();
  44.     name = aScript._name.toLowerCase();
  45.  
  46.     for (var i = 0, script; script = this._scripts[i]; i++) {
  47.       if (script._namespace.toLowerCase() == namespace
  48.         && script._name.toLowerCase() == name) {
  49.         return i;
  50.       }
  51.     }
  52.  
  53.     return -1;
  54.   },
  55.  
  56.   _load: function() {
  57.     var domParser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
  58.                               .createInstance(Components.interfaces.nsIDOMParser);
  59.  
  60.     var configContents = getContents(this._configFile);
  61.     var doc = domParser.parseFromString(configContents, "text/xml");
  62.     var nodes = doc.evaluate("/UserScriptConfig/Script", doc, null, 0, null);
  63.  
  64.     this._scripts = [];
  65.  
  66.     for (var node = null; node = nodes.iterateNext(); ) {
  67.       var script = new Script(this);
  68.  
  69.       for (var i = 0, childNode; childNode = node.childNodes[i]; i++) {
  70.         switch (childNode.nodeName) {
  71.         case "Include":
  72.           script._includes.push(childNode.firstChild.nodeValue);
  73.           break;
  74.         case "Exclude":
  75.           script._excludes.push(childNode.firstChild.nodeValue);
  76.           break;
  77.         case "Require":
  78.           var scriptRequire = new ScriptRequire(script);
  79.           scriptRequire._filename = childNode.getAttribute("filename");
  80.           script._requires.push(scriptRequire);
  81.           break;
  82.         case "Resource":
  83.           var scriptResource = new ScriptResource(script);
  84.           scriptResource._name = childNode.getAttribute("name");
  85.           scriptResource._filename = childNode.getAttribute("filename");
  86.           scriptResource._mimetype = childNode.getAttribute("mimetype");
  87.           scriptResource._charset = childNode.getAttribute("charset");
  88.           script._resources.push(scriptResource);
  89.           break;
  90.         case "Unwrap":
  91.           script._unwrap = true;
  92.           break;
  93.         }
  94.       }
  95.  
  96.       script._filename = node.getAttribute("filename");
  97.       script._name = node.getAttribute("name");
  98.       script._namespace = node.getAttribute("namespace");
  99.       script._description = node.getAttribute("description");
  100.       script._enabled = node.getAttribute("enabled") == true.toString();
  101.       script._basedir = node.getAttribute("basedir") || ".";
  102.  
  103.       this._scripts.push(script);
  104.     }
  105.   },
  106.  
  107.   _save: function() {
  108.     var doc = Components.classes["@mozilla.org/xmlextras/domparser;1"]
  109.       .createInstance(Components.interfaces.nsIDOMParser)
  110.       .parseFromString("<UserScriptConfig></UserScriptConfig>", "text/xml");
  111.  
  112.     for (var i = 0, scriptObj; scriptObj = this._scripts[i]; i++) {
  113.       var scriptNode = doc.createElement("Script");
  114.  
  115.       for (var j = 0; j < scriptObj._includes.length; j++) {
  116.         var includeNode = doc.createElement("Include");
  117.         includeNode.appendChild(doc.createTextNode(scriptObj._includes[j]));
  118.         scriptNode.appendChild(doc.createTextNode("\n\t\t"));
  119.         scriptNode.appendChild(includeNode);
  120.       }
  121.  
  122.       for (var j = 0; j < scriptObj._excludes.length; j++) {
  123.         var excludeNode = doc.createElement("Exclude");
  124.         excludeNode.appendChild(doc.createTextNode(scriptObj._excludes[j]));
  125.         scriptNode.appendChild(doc.createTextNode("\n\t\t"));
  126.         scriptNode.appendChild(excludeNode);
  127.       }
  128.  
  129.       for (var j = 0; j < scriptObj._requires.length; j++) {
  130.         var req = scriptObj._requires[j];
  131.         var resourceNode = doc.createElement("Require");
  132.  
  133.         resourceNode.setAttribute("filename", req._filename);
  134.  
  135.         scriptNode.appendChild(doc.createTextNode("\n\t\t"));
  136.         scriptNode.appendChild(resourceNode);
  137.       }
  138.  
  139.       for (var j = 0; j < scriptObj._resources.length; j++) {
  140.         var imp = scriptObj._resources[j];
  141.         var resourceNode = doc.createElement("Resource");
  142.  
  143.         resourceNode.setAttribute("name", imp._name);
  144.         resourceNode.setAttribute("filename", imp._filename);
  145.         resourceNode.setAttribute("mimetype", imp._mimetype);
  146.         if (imp._charset) {
  147.           resourceNode.setAttribute("charset", imp._charset);
  148.         }
  149.  
  150.         scriptNode.appendChild(doc.createTextNode("\n\t\t"));
  151.         scriptNode.appendChild(resourceNode);
  152.       }
  153.  
  154.       if (scriptObj._unwrap) {
  155.         scriptNode.appendChild(doc.createTextNode("\n\t\t"));
  156.         scriptNode.appendChild(doc.createElement("Unwrap"));
  157.       }
  158.  
  159.       scriptNode.appendChild(doc.createTextNode("\n\t"));
  160.  
  161.       scriptNode.setAttribute("filename", scriptObj._filename);
  162.       scriptNode.setAttribute("name", scriptObj._name);
  163.       scriptNode.setAttribute("namespace", scriptObj._namespace);
  164.       scriptNode.setAttribute("description", scriptObj._description);
  165.       scriptNode.setAttribute("enabled", scriptObj._enabled);
  166.       scriptNode.setAttribute("basedir", scriptObj._basedir);
  167.  
  168.       doc.firstChild.appendChild(doc.createTextNode("\n\t"));
  169.       doc.firstChild.appendChild(scriptNode);
  170.     }
  171.  
  172.     doc.firstChild.appendChild(doc.createTextNode("\n"));
  173.  
  174.     var configStream = getWriteStream(this._configFile);
  175.     Components.classes["@mozilla.org/xmlextras/xmlserializer;1"]
  176.       .createInstance(Components.interfaces.nsIDOMSerializer)
  177.       .serializeToStream(doc, configStream, "utf-8");
  178.     configStream.close();
  179.   },
  180.  
  181.   parse: function(source, uri) {
  182.     var ioservice = Components.classes["@mozilla.org/network/io-service;1"]
  183.                               .getService(Components.interfaces.nsIIOService);
  184.  
  185.     var script = new Script(this);
  186.     script._downloadURL = uri.spec;
  187.     script._enabled = true;
  188.  
  189.     // read one line at a time looking for start meta delimiter or EOF
  190.     var lines = source.match(/.+/g);
  191.     var lnIdx = 0;
  192.     var result = {};
  193.     var foundMeta = false;
  194.  
  195.     while ((result = lines[lnIdx++])) {
  196.       if (result.indexOf("// ==UserScript==") == 0) {
  197.         foundMeta = true;
  198.         break;
  199.       }
  200.     }
  201.  
  202.     // gather up meta lines
  203.     if (foundMeta) {
  204.       // used for duplicate resource name detection
  205.       var previousResourceNames = {};
  206.  
  207.       while ((result = lines[lnIdx++])) {
  208.         if (result.indexOf("// ==/UserScript==") == 0) {
  209.           break;
  210.         }
  211.  
  212.         var match = result.match(/\/\/ \@(\S+)(?:\s+([^\n]+))?/);
  213.         if (match === null) continue;
  214.  
  215.         var header = match[1];
  216.         var value = match[2];
  217.         if (value) { // @header <value>
  218.           switch (header) {
  219.             case "name":
  220.             case "namespace":
  221.             case "description":
  222.               script["_" + header] = value;
  223.               break;
  224.             case "include":
  225.               script._includes.push(value);
  226.               break;
  227.             case "exclude":
  228.               script._excludes.push(value);
  229.               break;
  230.             case "require":
  231.               var reqUri = ioservice.newURI(value, null, uri);
  232.               var scriptRequire = new ScriptRequire(script);
  233.               scriptRequire._downloadURL = reqUri.spec;
  234.               script._requires.push(scriptRequire);
  235.               break;
  236.             case "resource":
  237.               var res = value.match(/(\S+)\s+(.*)/);
  238.               if (res === null) {
  239.                 // NOTE: Unlocalized strings
  240.                 throw new Error("Invalid syntax for @resource declaration '" +
  241.                                 value + "'. Resources are declared like: " +
  242.                                 "@resource <name> <url>.");
  243.               }
  244.  
  245.               var resName = res[1];
  246.               if (previousResourceNames[resName]) {
  247.                 throw new Error("Duplicate resource name '" + resName + "' " +
  248.                                 "detected. Each resource must have a unique " +
  249.                                 "name.");
  250.               } else {
  251.                 previousResourceNames[resName] = true;
  252.               }
  253.  
  254.               var resUri = ioservice.newURI(res[2], null, uri);
  255.               var scriptResource = new ScriptResource(script);
  256.               scriptResource._name = resName;
  257.               scriptResource._downloadURL = resUri.spec;
  258.               script._resources.push(scriptResource);
  259.               break;
  260.           }
  261.         } else { // plain @header
  262.           switch (header) {
  263.             case "unwrap":
  264.               script._unwrap = true;
  265.               break;
  266.           }
  267.         }
  268.       }
  269.     }
  270.  
  271.     // if no meta info, default to reasonable values
  272.     if (script._name == null) script._name = parseScriptName(uri);
  273.     if (script._namespace == null) script._namespace = uri.host;
  274.     if (!script._description) script._description = "";
  275.     if (script._includes.length == 0) script._includes.push("*");
  276.  
  277.     return script;
  278.   },
  279.  
  280.   install: function(script) {
  281.     GM_log("> Config.install");
  282.  
  283.     var existingIndex = this._find(script);
  284.     if (existingIndex > -1) {
  285.       this.uninstall(this._scripts[existingIndex], false);
  286.     }
  287.  
  288.     script._initFile(script._tempFile);
  289.     script._tempFile = null;
  290.  
  291.     for (var i = 0; i < script._requires.length; i++) {
  292.       script._requires[i]._initFile();
  293.     }
  294.  
  295.     for (var i = 0; i < script._resources.length; i++) {
  296.       script._resources[i]._initFile();
  297.     }
  298.  
  299.     this._scripts.push(script);
  300.     this._changed(script, "install", null);
  301.  
  302.     GM_log("< Config.install");
  303.   },
  304.  
  305.   uninstall: function(script, uninstallPrefs) {
  306.     var idx = this._find(script);
  307.     this._scripts.splice(idx, 1);
  308.     this._changed(script, "uninstall", null);
  309.  
  310.     // watch out for cases like basedir="." and basedir="../gm_scripts"
  311.     if (!script._basedirFile.equals(this._scriptDir)) {
  312.       // if script has its own dir, remove the dir + contents
  313.       script._basedirFile.remove(true);
  314.     } else {
  315.       // if script is in the root, just remove the file
  316.       script._file.remove(false);
  317.     }
  318.  
  319.     if (uninstallPrefs) {
  320.       // Remove saved preferences
  321.       GM_prefRoot.remove("scriptvals." + script._namespace + "/" + script._name + ".");
  322.     }
  323.   },
  324.  
  325.   /**
  326.    * Moves an installed user script to a new position in the array of installed scripts.
  327.    *
  328.    * @param script The script to be moved.
  329.    * @param destination Can be either (a) a numeric offset for the script to be
  330.    *                    moved or (b) another installet script to which position
  331.    *                    the script will be moved.
  332.    */
  333.   move: function(script, destination) {
  334.     var from = this._scripts.indexOf(script);
  335.     var to = -1;
  336.  
  337.     // Make sure the user script is installed
  338.     if (from == -1) return;
  339.  
  340.     if (typeof destination == "number") { // if destination is an offset
  341.       to = from + destination;
  342.       to = Math.max(0, to);
  343.       to = Math.min(this._scripts.length - 1, to);
  344.     } else { // if destination is a script object
  345.       to = this._scripts.indexOf(destination);
  346.     }
  347.  
  348.     if (to == -1) return;
  349.  
  350.     var tmp = this._scripts.splice(from, 1)[0];
  351.     this._scripts.splice(to, 0, tmp);
  352.     this._changed(script, "move", to);
  353.   },
  354.  
  355.   get _scriptDir() {
  356.     var file = Components.classes["@mozilla.org/file/directory_service;1"]
  357.                          .getService(Components.interfaces.nsIProperties)
  358.                          .get("ProfD", Components.interfaces.nsILocalFile);
  359.     file.append("gm_scripts");
  360.     return file;
  361.   },
  362.  
  363.   /**
  364.    * Create an empty configuration if none exist.
  365.    */
  366.   _initScriptDir: function() {
  367.     var dir = this._scriptDir;
  368.  
  369.     if (!dir.exists()) {
  370.       dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
  371.  
  372.       var configStream = getWriteStream(this._configFile);
  373.       var xml = "<UserScriptConfig/>";
  374.       configStream.write(xml, xml.length);
  375.       configStream.close();
  376.     }
  377.   },
  378.  
  379.   get scripts() { return this._scripts.concat(); },
  380.   getMatchingScripts: function(testFunc) { return this._scripts.filter(testFunc); },
  381.  
  382.   /**
  383.    * Checks whether the version has changed since the last run and performs
  384.    * any necessary upgrades.
  385.    */
  386.   _updateVersion: function() {
  387.     log("> GM_updateVersion");
  388.  
  389.     // this is the last version which has been run at least once
  390.     var initialized = GM_prefRoot.getValue("version", "0.0");
  391.  
  392.     if ("0.0" == initialized) {
  393.       // this is the first launch.  show the welcome screen.
  394.  
  395.       // find an open window.
  396.       var windowManager = Components
  397.            .classes['@mozilla.org/appshell/window-mediator;1']
  398.            .getService(Components.interfaces.nsIWindowMediator);
  399.       var chromeWin = windowManager.getMostRecentWindow("navigator:browser");
  400.       // if we found it, use it to open a welcome tab
  401.       if (chromeWin.gBrowser) {
  402.         // the setTimeout makes sure we do not execute too early -- sometimes
  403.         // the window isn't quite ready to add a tab yet
  404.         chromeWin.setTimeout(
  405.             "gBrowser.selectedTab = gBrowser.addTab(" +
  406.             "'http://wiki.greasespot.net/Welcome')", 0);
  407.       }
  408.     }
  409.  
  410.     if (GM_compareVersions(initialized, "0.8") == -1)
  411.       this._pointEightBackup();
  412.  
  413.     // update the currently initialized version so we don't do this work again.
  414.     var extMan = Components.classes["@mozilla.org/extensions/manager;1"]
  415.       .getService(Components.interfaces.nsIExtensionManager);
  416.     var item = extMan.getItemForID(GM_GUID);
  417.     GM_prefRoot.setValue("version", item.version);
  418.  
  419.     log("< GM_updateVersion");
  420.   },
  421.  
  422.   /**
  423.    * In Greasemonkey 0.8 there was a format change to the gm_scripts folder and
  424.    * testing found several bugs where the entire folder would get nuked. So we
  425.    * are paranoid and backup the folder the first time 0.8 runs.
  426.    */
  427.   _pointEightBackup: function() {
  428.     var scriptDir = this._scriptDir;
  429.     var scriptDirBackup = scriptDir.clone();
  430.     scriptDirBackup.leafName += "_08bak";
  431.     if (scriptDir.exists() && !scriptDirBackup.exists())
  432.       scriptDir.copyTo(scriptDirBackup.parent, scriptDirBackup.leafName);
  433.   }
  434. };
  435.  
  436. function Script(config) {
  437.   this._config = config;
  438.   this._observers = [];
  439.  
  440.   this._downloadURL = null; // Only for scripts not installed
  441.   this._tempFile = null; // Only for scripts not installed
  442.   this._basedir = null;
  443.   this._filename = null;
  444.  
  445.   this._name = null;
  446.   this._namespace = null;
  447.   this._description = null;
  448.   this._enabled = true;
  449.   this._includes = [];
  450.   this._excludes = [];
  451.   this._requires = [];
  452.   this._resources = [];
  453.   this._unwrap = false;
  454. }
  455.  
  456. Script.prototype = {
  457.   matchesURL: function(url) {
  458.     function test(page) {
  459.       return convert2RegExp(page).test(url);
  460.     }
  461.  
  462.     return this._includes.some(test) && !this._excludes.some(test);
  463.   },
  464.  
  465.   _changed: function(event, data) { this._config._changed(this, event, data); },
  466.  
  467.   get name() { return this._name; },
  468.   get namespace() { return this._namespace; },
  469.   get description() { return this._description; },
  470.   get enabled() { return this._enabled; },
  471.   set enabled(enabled) { this._enabled = enabled; this._changed("edit-enabled", enabled); },
  472.  
  473.   get includes() { return this._includes.concat(); },
  474.   get excludes() { return this._excludes.concat(); },
  475.   addInclude: function(url) { this._includes.push(url); this._changed("edit-include-add", url); },
  476.   removeIncludeAt: function(index) { this._includes.splice(index, 1); this._changed("edit-include-remove", index); },
  477.   addExclude: function(url) { this._excludes.push(url); this._changed("edit-exclude-add", url); },
  478.   removeExcludeAt: function(index) { this._excludes.splice(index, 1); this._changed("edit-exclude-remove", index); },
  479.  
  480.   get requires() { return this._requires.concat(); },
  481.   get resources() { return this._resources.concat(); },
  482.   get unwrap() { return this._unwrap; },
  483.  
  484.   get _file() {
  485.     var file = this._basedirFile;
  486.     file.append(this._filename);
  487.     return file;
  488.   },
  489.  
  490.   get editFile() { return this._file; },
  491.  
  492.   get _basedirFile() {
  493.     var file = this._config._scriptDir;
  494.     file.append(this._basedir);
  495.     file.normalize();
  496.     return file;
  497.   },
  498.  
  499.   get fileURL() { return GM_getUriFromFile(this._file).spec; },
  500.   get textContent() { return getContents(this._file); },
  501.  
  502.   _initFileName: function(name, useExt) {
  503.     var ext = "";
  504.     name = name.toLowerCase();
  505.  
  506.     var dotIndex = name.lastIndexOf(".");
  507.     if (dotIndex > 0 && useExt) {
  508.       ext = name.substring(dotIndex + 1);
  509.       name = name.substring(0, dotIndex);
  510.     }
  511.  
  512.     name = name.replace(/\s+/g, "_").replace(/[^-_A-Z0-9]+/gi, "");
  513.     ext = ext.replace(/\s+/g, "_").replace(/[^-_A-Z0-9]+/gi, "");
  514.  
  515.     // If no Latin characters found - use default
  516.     if (!name) name = "gm_script";
  517.  
  518.     // 24 is a totally arbitrary max length
  519.     if (name.length > 24) name = name.substring(0, 24);
  520.  
  521.     if (ext) name += "." + ext;
  522.  
  523.     return name;
  524.   },
  525.  
  526.   _initFile: function(tempFile) {
  527.     var file = this._config._scriptDir;
  528.     var name = this._initFileName(this._name, false);
  529.  
  530.     file.append(name);
  531.     file.createUnique(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
  532.     this._basedir = file.leafName;
  533.  
  534.     file.append(name + ".user.js");
  535.     file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
  536.     this._filename = file.leafName;
  537.  
  538.     GM_log("Moving script file from " + tempFile.path + " to " + file.path);
  539.  
  540.     file.remove(true);
  541.     tempFile.moveTo(file.parent, file.leafName);
  542.   },
  543.  
  544.   get urlToDownload() { return this._downloadURL; },
  545.   setDownloadedFile: function(file) { this._tempFile = file; },
  546.  
  547.   get previewURL() {
  548.     return Components.classes["@mozilla.org/network/io-service;1"]
  549.                      .getService(Components.interfaces.nsIIOService)
  550.                      .newFileURI(this._tempFile).spec;
  551.   }
  552. };
  553.  
  554. function ScriptRequire(script) {
  555.   this._script = script;
  556.  
  557.   this._downloadURL = null; // Only for scripts not installed
  558.   this._tempFile = null; // Only for scripts not installed
  559.   this._filename = null;
  560. }
  561.  
  562. ScriptRequire.prototype = {
  563.   get _file() {
  564.     var file = this._script._basedirFile;
  565.     file.append(this._filename);
  566.     return file;
  567.   },
  568.  
  569.   get fileURL() { return GM_getUriFromFile(this._file).spec; },
  570.   get textContent() { return getContents(this._file); },
  571.  
  572.   _initFile: function() {
  573.     var name = this._downloadURL.substr(this._downloadURL.lastIndexOf("/") + 1);
  574.     if(name.indexOf("?") > 0) {
  575.       name = name.substr(0, name.indexOf("?"));
  576.     }
  577.     name = this._script._initFileName(name, true);
  578.  
  579.     var file = this._script._basedirFile;
  580.     file.append(name);
  581.     file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
  582.     this._filename = file.leafName;
  583.  
  584.     GM_log("Moving dependency file from " + this._tempFile.path + " to " + file.path);
  585.  
  586.     file.remove(true);
  587.     this._tempFile.moveTo(file.parent, file.leafName);
  588.     this._tempFile = null;
  589.   },
  590.  
  591.   get urlToDownload() { return this._downloadURL; },
  592.   setDownloadedFile: function(file) { this._tempFile = file; }
  593. };
  594.  
  595. function ScriptResource(script) {
  596.   this._script = script;
  597.  
  598.   this._downloadURL = null; // Only for scripts not installed
  599.   this._tempFile = null; // Only for scripts not installed
  600.   this._filename = null;
  601.   this._mimetype = null;
  602.   this._charset = null;
  603.  
  604.   this._name = null;
  605. }
  606.  
  607. ScriptResource.prototype = {
  608.   get name() { return this._name; },
  609.  
  610.   get _file() {
  611.     var file = this._script._basedirFile;
  612.     file.append(this._filename);
  613.     return file;
  614.   },
  615.  
  616.   get textContent() { return getContents(this._file); },
  617.  
  618.   get dataContent() {
  619.     var appSvc = Components.classes["@mozilla.org/appshell/appShellService;1"]
  620.                            .getService(Components.interfaces.nsIAppShellService);
  621.  
  622.     var window = appSvc.hiddenDOMWindow;
  623.     var binaryContents = getBinaryContents(this._file);
  624.  
  625.     var mimetype = this._mimetype;
  626.     if (this._charset && this._charset.length > 0) {
  627.       mimetype += ";charset=" + this._charset;
  628.     }
  629.  
  630.     return "data:" + mimetype + ";base64," +
  631.       window.encodeURIComponent(window.btoa(binaryContents));
  632.   },
  633.  
  634.   _initFile: ScriptRequire.prototype._initFile,
  635.  
  636.   get urlToDownload() { return this._downloadURL; },
  637.   setDownloadedFile: function(tempFile, mimetype, charset) {
  638.     this._tempFile = tempFile;
  639.     this._mimetype = mimetype;
  640.     this._charset = charset;
  641.   }
  642. };
  643.